package jp.syuriken.snsw.terenaibot;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.Random;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jp.syuriken.snsw.terenaibot.format.DataProviderFormatPrefix;
import jp.syuriken.snsw.terenaibot.format.FormatSyntaxException;
import jp.syuriken.snsw.terenaibot.format.PostFormatter;
import jp.syuriken.snsw.terenaibot.format.PostFormatter.ConstPrefixProperty;
import jp.syuriken.snsw.terenaibot.format.PrecompiledSyntax;
import jp.syuriken.snsw.terenaibot.internal.Clocker;
import jp.syuriken.snsw.terenaibot.internal.FollowFunction;
import jp.syuriken.snsw.terenaibot.internal.JobWorkerThread;
import jp.syuriken.snsw.terenaibot.internal.PluginContainerImpl;
import jp.syuriken.snsw.terenaibot.internal.StdoutPostWrapper;
import jp.syuriken.snsw.terenaibot.internal.ThreadHolder;
import jp.syuriken.snsw.terenaibot.internal.UnfollowFunction;
import jp.syuriken.snsw.terenaibot.mal.twitter.TwitterPostWrapper;
import jp.syuriken.snsw.terenaibot.plugins.FriendshipHookPlugin;
import jp.syuriken.snsw.terenaibot.plugins.PostHookPlugin;
import jp.syuriken.snsw.terenaibot.plugins.PrivateMessageHookPlugin;
import jp.syuriken.snsw.terenaibot.plugins.RegisterablePlugin;
import jp.syuriken.snsw.terenaibot.plugins.TimelineHookPlugin;
import jp.syuriken.snsw.terenaibot.plugins.WrapperStatusChangedHookPlugin;
import jp.syuriken.snsw.utils.Logger;
import jp.syuriken.snsw.utils.MyResourceBundle;

/**
 * TerenaiBotSystem コアクラス。
 * 
 * @author $Author: snswinhaiku.lo@gmail.com $
 * @version $Rev: 40 $
 */
public class BotSystem extends Thread {
	
	/**
	 * デバッグプラグイン
	 * 
	 * @author $Author: snswinhaiku.lo@gmail.com $ */
	@SuppressWarnings("unused")
	public class DebugPlugin implements RegisterablePlugin, FriendshipHookPlugin, WrapperStatusChangedHookPlugin,
			PrivateMessageHookPlugin, PostHookPlugin, TimelineHookPlugin {
		
		private Logger logger;
		

		@Override
		public void constructPlugin(BotConfiguration configuration) {
			logger = botConfiguration.getLogger();
		}
		
		@Override
		public void destructPlugin() {
			// do nothing
		}
		
		@Override
		public void exceedApiLimitation() {
			logger.log(Level.INFO, "Over API Limitation");
		}
		
		@Override
		public void exceedPostLimitation() {
			logger.log(Level.INFO, "Over Post Limitation");
		}
		
		@Override
		public String getPluginName() {
			return "DebugPlugin";
		}
		
		@Override
		public void hookAfterFollow(UserInfo userInfo) {
			// FollowBackPluginに任せる
		}
		
		@Override
		public void hookAfterPost(Post post) {
			logger.log(Level.FINER, String.format("Posted: %s", post.getPostText()));
		}
		
		@Override
		public void hookAfterSendPrivateMessage(Post2 privateMessage) {
			logger.log(
					Level.FINER,
					String.format("Send PrivateMessage to %s:, %s", privateMessage.getInReplyToUserId(),
							privateMessage.getPostText()));
		}
		
		@Override
		public void hookAfterUnfollow(UserInfo userInfo) {
			logger.log(Level.FINER, String.format("Unfollowed %s", userInfo.getScreenName()));
		}
		
		@Override
		public boolean hookBeforeFollow(String userId) {
			logger.log(Level.FINEST, String.format("hookBeforeFollow (userId=%s)", userId));
			return true;
		}
		
		@Override
		public boolean hookBeforePost(String postString) {
			logger.log(Level.FINEST, String.format("hookBeforePost (post=%s)", postString));
			return true;
		}
		
		@Override
		public boolean hookBeforeSendPrivateMessage(String poststr, String targetId) {
			logger.log(Level.FINEST,
					String.format("hookBeforeSendPrivateMessage (message=%s, target=%s)", poststr, targetId));
			return true;
		}
		
		@Override
		public boolean hookBeforeUnfollow(String userId) {
			logger.log(Level.FINEST, String.format("hookBeforeUnfollow (userId=%s)", userId));
			return true;
		}
		
		@Override
		public void hookGotPrivateMessage(Post2[] privateMessages) {
			for (Post2 post2 : privateMessages) {
				logger.log(Level.FINEST,
						String.format("Got PrivateMessage from %s: %s", post2.getUserName(), post2.getPostText()));
			}
		}
		
		@Override
		public void hookGotTimeline(Post2[] timeline) {
			for (Post2 post2 : timeline) {
				logger.log(Level.FINEST,
						String.format("Got Timeline from %s: %s", post2.getUserName(), post2.getPostText()));
			}
		}
		
		@Override
		public void hookNewFollower(UserInfo[] newFollowers) {
			for (UserInfo userInfo : newFollowers) {
				logger.log(Level.FINEST, String.format("being followed by %s", userInfo.getScreenName()));
			}
		}
		
		@Override
		public void hookNewUnfollower(UserInfo[] newUnfollowers) {
			for (UserInfo userInfo : newUnfollowers) {
				logger.log(Level.FINEST, String.format("being unfollowed by %s", userInfo.getScreenName()));
			}
		}
		
		@Override
		public void resetExceededApiLimitation() {
			logger.log(Level.FINEST, "Reset exceeded api limitation.");
		}
		
		@Override
		public void resetExceededPostLimitation() {
			logger.log(Level.FINEST, "Reset exceeded post limitation.");
		}
		
	}
	
	/**
	 * フォロー返しを実装するためのプラグイン
	 * 
	 * @author $Author: snswinhaiku.lo@gmail.com $
	 */
	@SuppressWarnings("unused")
	public class FollowBackPlugin implements RegisterablePlugin, FriendshipHookPlugin {
		
		private String[] followBackTypes = botConfiguration.getResourceBundle()
			.getString("core.followback.type", "blacklist").split(",");
		
		private TreeSet<String> blacklist = new TreeSet<String>();
		
		private boolean useBlacklist = false;
		
		private boolean useRatio = false;
		
		private boolean useFollow = false;
		
		private int maxFollow = 0;
		
		private int minFollow = 0;
		
		private double ratio = 0;
		
		private boolean followBackAuto;
		

		@Override
		public void constructPlugin(BotConfiguration configuration) {
			followBackAuto =
					Boolean.parseBoolean(botConfiguration.getResourceBundle().getString("core.followback", "false"));
			if (followBackAuto) {
				for (String followBackType : followBackTypes) {
					followBackType = followBackType.trim();
					if ("blacklist".equals(followBackType)) {
						useBlacklist = true;
						String[] blacklists =
								botConfiguration.getResourceBundle().getString("core.followback.blacklist").split(",");
						for (String string : blacklists) {
							blacklist.add(string);
						}
					} else if ("ratio".equals(followBackType)) {
						useRatio = true;
						ratio =
								Double.parseDouble(botConfiguration.getResourceBundle().getString(
										"core.followback.ratio"));
					} else if ("follow".equals(followBackType)) {
						useFollow = true;
						maxFollow =
								Integer.parseInt(botConfiguration.getResourceBundle().getString(
										"core.followback.follow.max", "-1"));
						if (maxFollow < 0) {
							maxFollow = Integer.MAX_VALUE;
						}
						minFollow =
								Integer.parseInt(botConfiguration.getResourceBundle().getString(
										"core.followback.follow.min", "-1"));
						/* if (minFollow < 0) {
							minFollow = Integer.MIN_VALUE;
						}*/
					} else {
						throw new RuntimeException("core.followback.type に blacklist, ratio, follow以外の値が指定されています。");
					}
				}
			}
		}
		
		@Override
		public void destructPlugin() {
			// do nothing
		}
		
		@Override
		public String getPluginName() {
			return "BotSystemHookPlugin";
		}
		
		@Override
		public void hookAfterFollow(UserInfo userInfo) {
			botConfiguration.getLogger().log(Level.INFO,
					String.format("%s (%s) followed", userInfo.getScreenName(), userInfo.getName()));
			try {
				botConfiguration.getWrapper().postInUserCall(
						getStringFromConfiguration(botConfiguration.getResourceBundle(), "core.followback.follownews"),
						userInfo);
			} catch (IllegalStateException e) {
				botConfiguration.getLogger().log(Level.INFO, "FollowBackPlugin#hookAfterFollow");
			} catch (WrapperException e) {
				botConfiguration.getLogger().log(Level.INFO, "FollowBackPlugin#hookAfterFollow");
			} catch (ConfigurationException e) {
				// do nothing
			}
		}
		
		@Override
		public void hookAfterUnfollow(UserInfo userInfo) {
			// do nothing
		}
		
		@Override
		public boolean hookBeforeFollow(String userId) {
			if (useBlacklist) {
				if (blacklist.contains(userId)) {
					return false;
				}
			}
			return true;
		}
		
		@Override
		public boolean hookBeforeUnfollow(String userId) {
			return true;
		}
		
		@Override
		public void hookNewFollower(UserInfo[] newFollowers) {
			if (followBackAuto == false) {
				return;
			}
			for (UserInfo userInfo : newFollowers) {
				if (useRatio) {
					if ((double) userInfo.getFollowersCount() / userInfo.getFriendsCount() < ratio) {
						continue;
					}
				}
				
				if (useFollow) {
					if ((minFollow <= userInfo.getFriendsCount() && userInfo.getFriendsCount() <= maxFollow) == false) {
						continue;
					}
				}
				try {
					botConfiguration.getWrapper().follow(userInfo);
				} catch (WrapperException e) {
					botConfiguration.getLogger().log(Level.WARNING, e, getPluginName());
				}
			}
		}
		
		@Override
		public void hookNewUnfollower(UserInfo[] newUnfollowers) {
			// TODO 
		}
	}
	
	private class MentionStruct {
		
		public Pattern regex;
		
		public PrecompiledSyntax precompiledSyntax;
		
		public TreeMap<String, String> typeMap;
		

		public MentionStruct(Pattern regex, PrecompiledSyntax precompiledSyntax, TreeMap<String, String> typeMap) {
			this.regex = regex;
			this.precompiledSyntax = precompiledSyntax;
			this.typeMap = typeMap;
		}
		
	}
	

	private static final String FORMAT_DATAFILE = "data/format_const.txt";
	
	/** タイムライン反応ポストデータファイル */
	private static final String TIMELINE_POSTDATA_FILE = "data/timeline.txt";
	
	/** メンションポストデータファイル */
	private static final String MENTION_POSTDATA_FILE = "data/mention.txt";
	
	/** ランダムポストデータファイル */
	private static final String RANDOM_POSTDATA_FILE = "data/randpost.txt";
	
	private static final int SEC_TO_MSEC = 1000;
	

	/**
	 * インスタンスを取得する (スレッドグループを作成する)
	 * 
	 * @param logger ロガー
	 * @param configFile 設定ファイル
	 * @param isDebug デバッグプラグインを埋め込むかどうか
	 * @return インスタンス
	 * @throws ConfigurationException 設定ファイルにエラーがあります
	 * @throws IOException IO例外が発生した
	 */
	public static BotSystem getInstance(Logger logger, File configFile, boolean isDebug) throws ConfigurationException,
			IOException {
		ThreadGroup threadGroup = new ThreadGroup("tbotsys");
		BotSystem instance = new BotSystem(threadGroup, logger, configFile, isDebug);
		return instance;
	}
	

	private ThreadHolder clockHolder;
	
	private Clocker clocker;
	
	private volatile boolean isFinish = false;
	
	private long wakeCount = 0;
	
	private int randomPostSpan;
	
	private String[] randomPostData;
	
	private Random random;
	
	private BotConfiguration botConfiguration;
	
	private PluginContainer pluginContainer;
	
	private long getMentionPostSpan;
	
	private MentionStruct[] mentionPostData;
	
	private long getFollowInfoSpan;
	
	private Pattern followBackRegex;
	
	private MentionStruct[] timelinePostData;
	
	private int getTimelineInfoSpan;
	
	private PostFormatter postFormatter;
	
	private String[] duplicateAvoidList;
	
	private int duplicateAvoidListIndex = 0;
	
	private int duplicateAvoidCount;
	
	private JobWorkerThread jobWorkerThread;
	

	/**
	 * インスタンスを生成する。
	 * @param threadGroup スレッドグループ
	 * @param logger ロガー
	 * @param configFile 設定ファイル。
	 * @throws FileNotFoundException ファイルが見つかりません
	 * @throws ConfigurationException 設定ファイルに問題があります。
	 * @throws IOException ロガーファイルが開けません。
	 */
	public BotSystem(ThreadGroup threadGroup, Logger logger, File configFile) throws ConfigurationException,
			IOException {
		super(threadGroup, "tbotsys-main"); // スレッド名の指定
		
		botConfiguration = new BotConfiguration();
		botConfiguration.setConfigurationFile(configFile);
		botConfiguration.setLogger(logger);
		logger.log(Level.INFO, "Initilaizing...");
		
		MyResourceBundle resourceBundle = new MyResourceBundle(botConfiguration.getConfigurationFile());
		botConfiguration.setConfigurationResource(resourceBundle);
		botConfiguration.setDataProviderFactory(new DataProviderFactory(botConfiguration, "data/var"));
		
		String postTo = getStringFromConfiguration(resourceBundle, "core.postto");
		PostWrapperPool.addWrapper2("twitter", TwitterPostWrapper.class);
		PostWrapperPool.addWrapper("stdout", StdoutPostWrapper.class);
		
		botConfiguration.setJobQueue(new JobQueue());
		startJobWorkerThread(threadGroup); // FindBugsの警告回避
		botConfiguration.getJobQueue().setJobWorkerThread(jobWorkerThread.getWorkerThreadHolder(), jobWorkerThread);
		
		pluginContainer = new PluginContainerImpl(botConfiguration, "plugin/");
		botConfiguration.setPluginContainer(pluginContainer);
		
		postFormatter = new PostFormatter(botConfiguration);
		loadConstPrefix();
		botConfiguration.setPostFormatter(postFormatter);
		
		PostWrapper2 postWrapper = null;
		try {
			postWrapper = PostWrapperPool.getWrapper(postTo, botConfiguration);
		} catch (InstantiationException e) {
			logger.log(Level.SEVERE, e, "PostWrapperを初期化できませんでした");
		} catch (IllegalAccessException e) {
			logger.log(Level.SEVERE, e, "PostWrapperを初期化できませんでした");
		}
		
		if (postWrapper == null) {
			throw new ConfigurationException(botConfiguration.getConfigurationFile(), "core.postto",
					ConfigurationExceptionType.ILLEGAL);
		}
		botConfiguration.setWrapper(postWrapper);
		
		clockHolder = new ThreadHolder();
		random = new Random();
		clocker =
				new Clocker(threadGroup, clockHolder, getLongFromConfiguration(resourceBundle, "clock.interval")
						* SEC_TO_MSEC);
		randomPostSpan = getIntFromConfiguration(resourceBundle, "post.random.span");
		getMentionPostSpan = getIntFromConfiguration(resourceBundle, "post.mention.span");
		getFollowInfoSpan = getIntFromConfiguration(resourceBundle, "post.follow.span");
		getTimelineInfoSpan = getIntFromConfiguration(resourceBundle, "post.timeline.span");
		
		postFormatter.addFunction("follow", new FollowFunction(postWrapper));
		postFormatter.addFunction("unfollow", new UnfollowFunction(postWrapper));
		postFormatter.addFormatPrefix("instance", new DataProviderFormatPrefix("instance"));
		
		loadRandomPost();
		loadMentionPost();
		loadTimelinePost();
		
		followBackRegex = Pattern.compile(getStringFromConfiguration(resourceBundle, "core.followback.regex"));
		
		logger.log(Level.CONFIG, " PostWrapper: " + postWrapper.getWrapperName());
		try {
			logger.log(Level.CONFIG, " User: " + postWrapper.getUserName());
		} catch (IllegalStateException e) {
			logger.log(Level.SEVERE, "PostWrapperでエラー: " + e.getLocalizedMessage());
			System.err.println("正しくポスト先に接続出来ていない可能性があります。もう一度設定を確認してください。");
		} catch (WrapperException e) {
			logger.log(Level.WARNING, "ユーザー名の取得でエラーが発生しました。");
			logger.log(Level.FINE, e.getCause());
		}
		logger.log(Level.CONFIG, " Loaded mutter files:");
		logger.log(Level.CONFIG, "  random post: " + randomPostData.length);
		logger.log(Level.CONFIG, "  Mention: " + mentionPostData.length);
		logger.log(Level.CONFIG, "  timeline post: " + timelinePostData.length);
		
		FollowBackPlugin followBackPlugin = new FollowBackPlugin();
		followBackPlugin.constructPlugin(botConfiguration);
		pluginContainer.addPlugin(followBackPlugin);
		pluginContainer.constructPlugins(botConfiguration);
		
		duplicateAvoidCount = getIntFromConfiguration(resourceBundle, "core.duplicate_avoid_count");
		duplicateAvoidCount =
				duplicateAvoidCount >= randomPostData.length ? randomPostData.length - 1 : duplicateAvoidCount;
		duplicateAvoidList = new String[duplicateAvoidCount];
		for (int i = 0; i < duplicateAvoidCount; i++) {
			duplicateAvoidList[i] = null;
		}
		logger.log(Level.INFO, "Initilaized.");
	}
	
	/**
	 * インスタンスを生成する。
	 * @param threadGroup スレッドグループ
	 * @param logger ロガー
	 * @param file 設定ファイル
	 * @param isDebug デバッグプラグインを埋め込むかどうか
	 * @throws IOException 設定ファイルの読み込み中にIOエラーが発生した
	 * @throws ConfigurationException 設定ファイルが正しくない
	 */
	public BotSystem(ThreadGroup threadGroup, Logger logger, File file, boolean isDebug) throws ConfigurationException,
			IOException {
		this(threadGroup, logger, file);
		if (isDebug) {
			DebugPlugin debugPlugin = new DebugPlugin();
			debugPlugin.constructPlugin(botConfiguration);
			pluginContainer.addPlugin(debugPlugin);
		}
	}
	
	private int getIntFromConfiguration(MyResourceBundle resourceBundle, String configName)
			throws ConfigurationException {
		try {
			return Integer.parseInt(getStringFromConfiguration(resourceBundle, configName));
		} catch (NumberFormatException e) {
			throw new ConfigurationException(botConfiguration.getConfigurationFile(), configName,
					ConfigurationExceptionType.NOT_DECIMAL);
		}
	}
	
	private long getLongFromConfiguration(MyResourceBundle resourceBundle, String configName)
			throws ConfigurationException {
		try {
			return Long.parseLong(getStringFromConfiguration(resourceBundle, configName));
		} catch (NumberFormatException e) {
			throw new ConfigurationException(botConfiguration.getConfigurationFile(), configName,
					ConfigurationExceptionType.NOT_DECIMAL);
		}
	}
	
	private String getStringFromConfiguration(MyResourceBundle resourceBundle, String configName)
			throws ConfigurationException {
		try {
			return resourceBundle.getString(configName);
		} catch (MissingResourceException e) {
			throw new ConfigurationException(botConfiguration.getConfigurationFile(), configName,
					ConfigurationExceptionType.NOT_FOUND);
		}
	}
	
	/**
	 * タイムラインを処理する。
	 * 
	 * @param timeline 処理するタイムライン
	 */
	private void handlePosts(Post2[] timeline) {
		String userId = null;
		try {
			userId = botConfiguration.getWrapper().getUserInfo().getUserId();
		} catch (WrapperException e) {
			botConfiguration.getLogger().log(Level.WARNING, e);
		}
		for (Post2 post : timeline) {
			if (post.getUser().equals(userId) || post.isRepost()) {
				// 自分自身またはRTされたポストは無視する。
				continue;
			}
			switch (post.getPostType()) {
				case UNICAST:
					if (followBackRegex.matcher(post.getPostActualText()).matches()) {
						try {
							botConfiguration.getWrapper().follow(post.getUser());
						} catch (WrapperException e) {
							botConfiguration.getLogger().log(Level.INFO, e, "PostWrapper#follow");
						}
						continue;
					}
					// fall through
				case MULTICAST:
					ArrayList<MentionStruct> mentionStructs = new ArrayList<MentionStruct>();
					ArrayList<MentionStruct> lowPriorityMentionStructs = new ArrayList<MentionStruct>();
					for (MentionStruct mentionStruct : mentionPostData) {
						Matcher matcher = mentionStruct.regex.matcher(post.getPostActualText());
						if (matcher.matches()) {
							String priority = mentionStruct.typeMap.get("priority");
							if (priority == null) {
								priority = "default";
							}
							if (priority.equals("low")) {
								lowPriorityMentionStructs.add(mentionStruct);
							} else {
								mentionStructs.add(mentionStruct);
							}
						}
					}
					
					MentionStruct mention = null;
					if (mentionStructs.size() != 0) {
						mention = mentionStructs.get(random.nextInt(mentionStructs.size()));
					} else if (lowPriorityMentionStructs.size() != 0) {
						mention = lowPriorityMentionStructs.get(random.nextInt(lowPriorityMentionStructs.size()));
					} else {
						continue;
					}
					try {
						Matcher matcher = mention.regex.matcher(post.getPostActualText());
						matcher.matches();
						botConfiguration.getWrapper().postBackgroundInReply(mention.precompiledSyntax, matcher, post);
					} catch (IllegalStateException e) {
						botConfiguration.getLogger().log(Level.WARNING, e);
					}
					break;
				case DEFAULT:
					ArrayList<MentionStruct> timelinePosts = new ArrayList<MentionStruct>();
					for (MentionStruct timelineMentionStruct : timelinePostData) {
						Matcher matcher = timelineMentionStruct.regex.matcher(post.getPostText());
						if (matcher.matches()) {
							timelinePosts.add(timelineMentionStruct);
						}
					}
					if (timelinePosts.size() != 0) {
						mention = timelinePosts.get(random.nextInt(timelinePosts.size()));
					} else {
						continue;
					}
					try {
						botConfiguration.getWrapper().postBackgroundInReply(mention.precompiledSyntax,
								mention.regex.matcher(post.getPostActualText()), post);
					} catch (IllegalStateException e) {
						botConfiguration.getLogger().log(Level.WARNING, e);
					}
					break;
				case PRIVATE_MESSAGE:
					break;
				default:
					break;
			}
		}
	}
	
	/**
	 * 定数値KVを読み込む
	 */
	private void loadConstPrefix() {
		ConstPrefixProperty constPrefix = (ConstPrefixProperty) postFormatter.getPrefix("const");
		Properties properties = new Properties();
		FileReader reader = null;
		try {
			reader = new FileReader(FORMAT_DATAFILE);
			properties.load(reader);
		} catch (FileNotFoundException e) {
			botConfiguration.getLogger().log(Level.WARNING, FORMAT_DATAFILE + " が開けませんでした。" + e.getLocalizedMessage());
		} catch (IOException e) {
			botConfiguration.getLogger().log(Level.WARNING, e, FORMAT_DATAFILE + " の読み込み中にエラー");
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					botConfiguration.getLogger().log(Level.WARNING, e, FORMAT_DATAFILE + " のクローズ中にエラー");
				}
			}
		}
		for (String key : properties.stringPropertyNames()) {
			String value = properties.getProperty(key, "");
			constPrefix.addConst(key, value);
		}
	}
	
	/**
	 * Mentionデータファイルを読み込む
	 */
	private void loadMentionPost() {
		ArrayList<MentionStruct> mentionPostDataList = new ArrayList<MentionStruct>();
		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new FileReader(MENTION_POSTDATA_FILE));
			while (true) {
				String line = reader.readLine();
				if (line == null) {
					break;
				}
				line = line.trim();
				if (line.startsWith("#") || line.isEmpty()) {
					continue;
				}
				// regex;Type[,Type...];mutter...
				int regexSeparator = line.indexOf(";");
				if (regexSeparator < 0) {
					botConfiguration.getLogger().log(Level.WARNING, "Mention反応ファイルの読み込み中にエラー (正しくない形式): " + line);
					continue;
				}
				String regexString = line.substring(0, regexSeparator).trim();
				
				int typeSeparator = line.indexOf(";", regexSeparator + 1);
				if (typeSeparator < 0) {
					botConfiguration.getLogger().log(Level.WARNING, "Mention反応ファイルの読み込み中にエラー (正しくない形式): " + line);
					continue;
				}
				String[] typeString = line.substring(regexSeparator + 1, typeSeparator).split(",");
				String mutterString = line.substring(typeSeparator + 1).trim();
				if (mutterString.equals("<<<")) {
					StringBuilder stringBuilder = new StringBuilder();
					while (true) {
						String subLineString = reader.readLine();
						if (subLineString == null) {
							botConfiguration.getLogger().log(Level.SEVERE, "data/mention.txt: Unexpected EOF");
						} else if (subLineString.equals(">>>")) {
							break;
						} else {
							stringBuilder.append(subLineString.trim());
						}
					}
					mutterString = stringBuilder.toString();
				}
				PrecompiledSyntax precompiledSyntax;
				try {
					precompiledSyntax = postFormatter.precompile(mutterString);
				} catch (FormatSyntaxException e) {
					botConfiguration.getLogger().log(Level.WARNING,
							String.format("%s: %s", e.getLocalizedMessage(), mutterString));
					continue;
				}
				
				TreeMap<String, String> typeMap = new TreeMap<String, String>();
				for (String type : typeString) {
					if (type.trim().isEmpty()) {
						continue;
					}
					String[] strings = type.split("=");
					if (strings.length != 2) {
						botConfiguration.getLogger().log(Level.WARNING,
								"Mention反応ファイルの読み込み中にエラー (typeが正しくない形式です): " + type);
						continue;
					} else {
						typeMap.put(strings[0].trim(), strings[1].trim());
					}
				}
				if (regexString.isEmpty()) {
					if (typeMap.containsKey("priority") == false) {
						typeMap.put("priority", "low");
					}
					regexString = ".*";
				}
				mentionPostDataList.add(new MentionStruct(Pattern.compile(regexString), precompiledSyntax, typeMap));
			}
		} catch (FileNotFoundException e) {
			botConfiguration.getLogger().log(Level.WARNING,
					RANDOM_POSTDATA_FILE + " が見つかりませんでした。" + e.getLocalizedMessage());
		} catch (IOException e) {
			botConfiguration.getLogger().log(Level.WARNING, e, RANDOM_POSTDATA_FILE + "を読み込んでいるときにエラー。");
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					System.err.println(e.getLocalizedMessage());
				}
			}
		}
		mentionPostData = mentionPostDataList.toArray(new MentionStruct[mentionPostDataList.size()]);
	}
	
	private void loadRandomPost() {
		ArrayList<String> randomPostDataList = new ArrayList<String>();
		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new FileReader(RANDOM_POSTDATA_FILE));
			while (true) {
				String line = reader.readLine();
				if (line == null) {
					break;
				}
				line = line.trim();
				if (line.startsWith("#") || line.isEmpty()) {
					continue;
				}
				
				randomPostDataList.add(line);
			}
		} catch (FileNotFoundException e) {
			botConfiguration.getLogger().log(Level.WARNING, RANDOM_POSTDATA_FILE + " が見つかりませんでした。");
		} catch (IOException e) {
			botConfiguration.getLogger().log(Level.WARNING, e, RANDOM_POSTDATA_FILE + "の読み込み中にエラー");
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					System.err.println(e.getLocalizedMessage());
				}
			}
		}
		randomPostData = randomPostDataList.toArray(new String[randomPostDataList.size()]);
	}
	
	private void loadTimelinePost() {
		ArrayList<MentionStruct> timelinePostDataList = new ArrayList<MentionStruct>();
		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new FileReader(TIMELINE_POSTDATA_FILE));
			while (true) {
				String line = reader.readLine();
				if (line == null) {
					break;
				}
				line = line.trim();
				if (line.startsWith("#") || line.isEmpty()) {
					continue;
				}
				// regex;Type[,Type...];mutter...
				int regexSeparator = line.indexOf(";");
				if (regexSeparator < 0) {
					botConfiguration.getLogger().log(Level.WARNING, "Timeline反応ファイルの読み込み中にエラー (正しくない形式): " + line);
					continue;
				}
				String regexString = line.substring(0, regexSeparator).trim();
				
				int typeSeparator = line.indexOf(";", regexSeparator + 1);
				if (typeSeparator < 0) {
					botConfiguration.getLogger().log(Level.WARNING, "Timeline反応ファイルの読み込み中にエラー (正しくない形式): " + line);
					continue;
				}
				String[] typeString = line.substring(regexSeparator + 1, typeSeparator).split(",");
				String mutterString = line.substring(typeSeparator + 1).trim();
				if (mutterString.equals("<<<")) {
					StringBuilder stringBuilder = new StringBuilder();
					while (true) {
						String subLineString = reader.readLine();
						if (subLineString == null) {
							botConfiguration.getLogger().log(Level.SEVERE, "data/timeline.txt: Unexpected EOF");
						} else if (subLineString.equals(">>>")) {
							break;
						} else {
							stringBuilder.append(subLineString.trim());
						}
					}
					mutterString = stringBuilder.toString();
				}
				PrecompiledSyntax precompiledSyntax;
				try {
					precompiledSyntax = postFormatter.precompile(mutterString);
				} catch (FormatSyntaxException e) {
					botConfiguration.getLogger().log(Level.WARNING,
							String.format("%s: %s", e.getLocalizedMessage(), mutterString));
					continue;
				}
				
				TreeMap<String, String> typeMap = new TreeMap<String, String>();
				for (String type : typeString) {
					if (type.trim().isEmpty()) {
						continue;
					}
					String[] strings = type.split("=");
					if (strings.length != 2) {
						botConfiguration.getLogger().log(Level.WARNING,
								"Mention反応ファイルの読み込み中にエラー (typeが正しくない形式です): " + type);
						continue;
					} else {
						typeMap.put(strings[0].trim(), strings[1].trim());
					}
				}
				timelinePostDataList.add(new MentionStruct(Pattern.compile(regexString), precompiledSyntax, typeMap));
			}
		} catch (FileNotFoundException e) {
			botConfiguration.getLogger().log(Level.WARNING,
					TIMELINE_POSTDATA_FILE + " が見つかりませんでした。" + e.getLocalizedMessage());
		} catch (IOException e) {
			botConfiguration.getLogger().log(Level.WARNING, e, TIMELINE_POSTDATA_FILE + "を読み込んでいるときにエラー。");
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					System.err.println(e.getLocalizedMessage());
				}
			}
		}
		timelinePostData = timelinePostDataList.toArray(new MentionStruct[timelinePostDataList.size()]);
	}
	
	/**
	 * ボットのアクションを選択する
	 */
	private void nextBotAction() {
		if (wakeCount % randomPostSpan == 0) {
			postByRandom();
		}
		if (wakeCount % getMentionPostSpan == 0) {
			try {
				handlePosts(botConfiguration.getWrapper().getMentions());
			} catch (WrapperException2 e) {
				botConfiguration.getLogger().log(Level.INFO, e);
			}
		}
		if (wakeCount % getFollowInfoSpan == 0) {
			try {
				botConfiguration.getWrapper().getFollowers(true);
				botConfiguration.getWrapper().getFollowing(true);
			} catch (WrapperException2 e) {
				botConfiguration.getLogger().log(Level.INFO, e);
			}
		}
		if (wakeCount % getTimelineInfoSpan == 0) {
			try {
				handlePosts(botConfiguration.getWrapper().getTimeline());
			} catch (WrapperException2 e) {
				botConfiguration.getLogger().log(Level.INFO, e);
			}
		}
	}
	
	/**
	 * ランダムにポストを行う
	 */
	private void postByRandom() {
		if (randomPostData.length != 0) {
			String postString;
			boolean flag;
			do {
				postString = randomPostData[random.nextInt(randomPostData.length)];
				flag = true;
				for (String str : duplicateAvoidList) {
					if (postString.equals(str)) {
						flag = false;
					}
				}
			} while (!flag);
			duplicateAvoidList[duplicateAvoidListIndex++] = postString;
			if (duplicateAvoidListIndex >= duplicateAvoidCount) {
				duplicateAvoidListIndex = 0;
			}
			try {
				botConfiguration.getWrapper().postBackground(postString);
			} catch (IllegalStateException e) {
				botConfiguration.getLogger().log(Level.WARNING, e, "BotSystem#postByRandom");
			}
		}
	}
	
	@Override
	public void run() {
		clocker.start();
		while (!isFinish) {
			try {
				nextBotAction();
			} catch (RuntimeException e) {
				botConfiguration.getLogger().log(Level.SEVERE, e, "Not caught exception (thread tbotsys-main)");
			}
			wakeCount++;
			try {
				synchronized (clockHolder) {
					if (!isFinish) {
						clockHolder.wait();
					}
				}
			} catch (InterruptedException e) {
				// do nothing
			}
		}
		clocker.stopClocker();
		try {
			clocker.join();
		} catch (InterruptedException e) {
			// do nothing
		}
	}
	
	private void startJobWorkerThread(ThreadGroup threadGroup) {
		jobWorkerThread = new JobWorkerThread(threadGroup, botConfiguration);
		jobWorkerThread.start();
	}
	
	/**
	 * ボットを停止する。実際に停止したかどうかは{@link #join()}などを使ってください。
	 */
	public void stopBot() {
		clocker.stopClocker();
		isFinish = true;
		jobWorkerThread.quit();
		botConfiguration.getDataProviderFactory().store();
		pluginContainer.destructPlugins();
		
		ThreadHolder threadHolder = jobWorkerThread.getWorkerThreadHolder();
		synchronized (threadHolder) {
			if (threadHolder != null) {
				threadHolder.notifyAll();
			}
		}
		try {
			jobWorkerThread.join();
		} catch (InterruptedException e) {
			//do nothing
		}
	}
}
